/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
    Stmt,
    ArkMethod,
    FullPosition, ArkReturnStmt, ClassType, UnionType, ts
} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {ClassMatcher, MatcherCallback, MatcherTypes, MethodMatcher} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {BaseChecker, BaseMetaData} from "../../checker/BaseChecker";
import {RuleListUtil} from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeReturnCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: "docs/no-unsafe-return-check.md", // TODO: support url
    description: "Disallow calling a function with a value with type `any`"
};

export class NoUnsafeReturnCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };

    private buildMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.clsMatcher],
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildTs: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        }
        return [matchBuildTs];
    }

    public check = (targetMethod: ArkMethod) => {
        const stmts = targetMethod.getBody()?.getCfg().getStmts() ?? [];
        if (!this.getFileExtension(targetMethod.getDeclaringArkClass().getDeclaringArkFile().getName(), 'ts')) {
            return;
        }
        for (const stmt of stmts) {
            if (stmt instanceof ArkReturnStmt) {
                if (stmt.getOp()) {
                    if (stmt.getOp().getType() instanceof ClassType) {
                        if ((stmt.getOp().getType() as ClassType).getClassSignature().getClassName() !== 'Map' && (stmt.getOp().getType() as ClassType).getClassSignature().getClassName() !== 'Set') {
                            return;
                        }
                    }
                    if (stmt.getOp().getType() instanceof UnionType) {
                        return;
                    }
                }
                const code = stmt.getOriginalText() || '';
                const ast = ts.createSourceFile('example.ts', code, ts.ScriptTarget.Latest, true);
                let foundAnyKeyword = false;
                ts.forEachChild(ast, node => {
                    if (ts.isReturnStatement(node)) {
                        if (node.expression && ts.isAsExpression(node.expression)) {
                            if (ts.isTypeReferenceNode(node.expression.type)) {
                                if (node.expression.type.typeArguments?.some(typeArg => typeArg.kind !== ts.SyntaxKind.AnyKeyword)) {
                                    foundAnyKeyword = true;
                                }
                            }
                        }
                    }
                    if (ts.isExpressionStatement(node)) {
                        if (ts.isArrayLiteralExpression(node.expression)) {
                            if (node.expression.elements.length === 0) {
                                foundAnyKeyword = true;
                            }
                        }
                    }
                });
                if (foundAnyKeyword) {
                    return;
                }
                if (stmt.getOp().getType().getTypeString().includes("any")) {
                    if (!this.checkUnknownBeforeBrace(stmt.getCfg().getDeclaringMethod().getCode() ?? '')) {
                        this.addIssueReport(stmt);
                    }
                }

            }
        }
    }

    private checkUnknownBeforeBrace(code: string): boolean {
        const regex = /unknown\s*\{/;
        return regex.test(code);
    }

    private getFileExtension(filePath: string, filetype: string): boolean {
        const match = filePath.match(/\.([0-9a-zA-Z]+)$/);
        if (match) {
            const extension = match[1];
            return extension === filetype;
        }
        return false;
    }

    private addIssueReport(stmt: Stmt) {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(stmt);
        const fixKey = warnInfo.line + '%' + warnInfo.startCol + '%' + this.rule.ruleId;
        const mergeKey = warnInfo.filePath + '%' + fixKey + '%' + this.metaData.description;
        const defect = new Defects(warnInfo.line!, warnInfo.startCol, this.metaData.description, severity,
            this.rule.ruleId, mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey)
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }

    private getLineAndColumn(stmt: Stmt) {
        const originalPositions = stmt.getOperandOriginalPositions();
        let position: FullPosition | undefined = undefined;
        if (originalPositions![0]) {
            position = originalPositions![0];
        }
        if (position) {
            const line = position.getFirstLine();
            const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
            if (arkFile) {
                let startCol = position.getFirstCol();
                const endCol = position.getFirstCol();
                const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
                return {line, startCol, endCol, filePath: filePath}
            } else {
                logger.debug('originStmt or arkFile is null');
            }
        }
        return {line: -1, startCol: -1, endCol: -1, filePath: ''};
    }
}